import * as Excel from "exceljs";
import json5 = require("json5");

import * as log from "./helpers/log";

const toNormalizedString = (str: any): string => {
	if (typeof str != "string") {
		return JSON.stringify(str);
	}

	return str
		.replace(/\r/g, "")
		.replace("_x000D_", "")
		.trim();
};

export const asJson = (cell: Excel.Cell): any => {
	const cellText = toNormalizedString(cell.text);

	if (0 == cellText.length) {
		return undefined;
	}

	try {
		return json5.parse(toNormalizedString(cell.text));
	} catch (err) {
		log.e(err);
		return undefined;
	}
};

export const asString = (cell: Excel.Cell): string => {
	return toNormalizedString(cell.text);
};

export const asNumber = (cell: Excel.Cell): number => {
	try {
		const finalNumber = Number(toNormalizedString(cell.text));

		if (isNaN(finalNumber)) {
			log.e(`${cell.text} at ${cell.$col$row}: not a number.`);
			return 0;
		} else {
			return finalNumber;
		}
	} catch (err) {
		log.e(`${cell.text} at ${cell.$col$row}: ${err}`);
		return 0;
	}
};

export const asNullableInt = (cell: Excel.Cell): number | null => {
	try {
		const finalNumber = parseInt(toNormalizedString(cell.text));

		if (isNaN(finalNumber)) {
			log.d(`${cell.text} at ${cell.$col$row}: not a number.`);
			return null;
		} else {
			return finalNumber;
		}
	} catch (err) {
		log.d(`${cell.text} at ${cell.$col$row}: ${err}`);
		return null;
	}
};

export const asBoolean = (cell: Excel.Cell): boolean => {
	try {
		const lowerString = toNormalizedString(cell.text).toLowerCase();
		if (
			"" == lowerString ||
			"false" == lowerString ||
			"0" == lowerString ||
			"null" == lowerString ||
			"undefined" == lowerString
		) {
			return false;
		} else {
			return Boolean(toNormalizedString(cell.text));
		}
	} catch (err) {
		log.e(err);
		return false;
	}
};

const parseStringAsStringArray = (str: string, sep?: string): string[] => {
	if (0 === str.length) {
		return [];
	}

	if (!sep) {
		sep = "|";
	}

	let strings = str.split(sep);

	for (const key in strings) {
		strings[key] = strings[key].trim();
	}

	return strings;
};

const parseStringAsNumberArray = (str: string, sep?: string): number[] => {
	const strings = parseStringAsStringArray(str, sep);

	let numbers: number[] = [];

	for (const key in strings) {
		const num = Number(strings[key]);

		if (isNaN(num)) {
			log.e(`${strings[key]}: not a number.`);
			numbers[key] = 0;
		} else {
			numbers[key] = num;
		}
	}

	return numbers;
};

const parseStringAsBooleanArray = (str: string, sep?: string): boolean[] => {
	const strings = parseStringAsStringArray(str, sep);

	let booleans: boolean[] = [];

	for (const key in strings) {
		booleans[key] = Boolean(strings[key]);
	}

	return booleans;
};

export const asStringArray = (cell: Excel.Cell): string[] => {
	return parseStringAsStringArray(asString(cell));
};

export const asNumberArray = (cell: Excel.Cell): number[] => {
	return parseStringAsNumberArray(asString(cell));
};

export const asBooleanArray = (cell: Excel.Cell): boolean[] => {
	return parseStringAsBooleanArray(asString(cell));
};

export const asStringArray2D = (cell: Excel.Cell): string[][] => {
	const lev1arr = asStringArray(cell);
	const ret: string[][] = [];

	for (const key in lev1arr) {
		ret[key] = parseStringAsStringArray(lev1arr[key], "`");
	}

	return ret;
};

export const asNumberArray2D = (cell: Excel.Cell): number[][] => {
	const lev1arr = asStringArray(cell);
	const ret: number[][] = [];

	for (const key in lev1arr) {
		ret[key] = parseStringAsNumberArray(lev1arr[key], "`");
	}

	return ret;
};

export const asBooleanArray2D = (cell: Excel.Cell): boolean[][] => {
	const lev1arr = asStringArray(cell);
	const ret: boolean[][] = [];

	for (const key in lev1arr) {
		ret[key] = parseStringAsBooleanArray(lev1arr[key], "`");
	}

	return ret;
};

export enum Type {
	Json,
	Number,
	String,
	Boolean,
	NumberArray,
	StringArray,
	BooleanArray,
	NumberArray2D,
	StringArray2D,
	BooleanArray2D
}

const typeToTsTypeStringMap = new Map([
	[Type.Json, "any"],
	[Type.Number, "number"],
	[Type.String, "string"],
	[Type.Boolean, "boolean"],
	[Type.NumberArray, "number[]"],
	[Type.StringArray, "string[]"],
	[Type.BooleanArray, "boolean[]"],
	[Type.NumberArray2D, "number[][]"],
	[Type.StringArray2D, "string[][]"],
	[Type.BooleanArray2D, "boolean[][]"]
]);

const isJsonType = (typeString: string) => {
	return typeString === "json";
};

const isNumberType = (typeString: string) => {
	return (
		typeString.startsWith("byte") ||
		typeString.startsWith("short") ||
		typeString.startsWith("int") ||
		typeString.startsWith("long") ||
		typeString.startsWith("float") ||
		typeString.startsWith("double") ||
		typeString.startsWith("num") ||
		false
	);
};

const isBooleanType = (typeString: string) => {
	return typeString.startsWith("bool");
};

const isArrayType = (typeString: string) => {
	return (
		typeString.endsWith("arr") ||
		typeString.endsWith("array") ||
		typeString.endsWith("arr1d") ||
		typeString.endsWith("array1d") ||
		false
	);
};

const isArray2DType = (typeString: string) => {
	return typeString.endsWith("arr2d") || typeString.endsWith("array2d") || false;
};

export const getTypeFromTypeString = (typeString: string): Type => {
	if (!typeString || typeString.length < 1) {
		return Type.String;
	}

	typeString = typeString.toLowerCase();

	if (isJsonType(typeString)) {
		return Type.Json;
	}

	const typeStringIsNumberType = isNumberType(typeString);
	const typeStringIsBooleanType = isBooleanType(typeString);
	const typeStringIsArrayType = isArrayType(typeString);
	const typeStringIsArray2DType = isArray2DType(typeString);

	if (typeStringIsNumberType) {
		if (typeStringIsArray2DType) {
			return Type.NumberArray2D;
		} else if (typeStringIsArrayType) {
			return Type.NumberArray;
		} else {
			return Type.Number;
		}
	}

	if (typeStringIsBooleanType) {
		if (typeStringIsArray2DType) {
			return Type.BooleanArray2D;
		} else if (typeStringIsArrayType) {
			return Type.BooleanArray;
		} else {
			return Type.Boolean;
		}
	}

	if (typeStringIsArray2DType) {
		return Type.StringArray2D;
	} else if (typeStringIsArrayType) {
		return Type.StringArray;
	} else {
		return Type.String;
	}
};

export const typeToTsTypeString = (type: Type) => {
	if (typeToTsTypeStringMap.has(type)) {
		return typeToTsTypeStringMap.get(type);
	} else {
		log.e(`未知类型 ${type} 。`);
		return "any";
	}
};
